
namespace Stee.CAP8.DBAccess
{
    using Microsoft.EntityFrameworkCore;
    using System;
    using System.Threading.Tasks;
    using Microsoft.Extensions.Configuration;
    using Stee.CAP8.Entity;
    using System.Linq;
    using Microsoft.Extensions.Logging;
    using NLog.Extensions.Logging;

    public class UnitOfWork<TContext> : IUnitOfWork<TContext> where TContext : DbContext
    {
        public TContext Context { get; private set; }
        internal static readonly ILoggerFactory NlogLoggerFactory
            = LoggerFactory.Create(builder => {
                builder
                .AddFilter((category, level) =>
                    category == DbLoggerCategory.Database.Command.Name
                    && (level == LogLevel.Warning || level == LogLevel.Error ||  level == LogLevel.Critical))
                .AddNLog("nlog.config"); });
        /// <summary>
        /// The constructor will retrieve connection string from the program's appsettings.json and set it into TContext's connection.
        /// <para>setting <paramref name="useOracle"/> to true will use Oracle as database. </para>
        /// <para>setting <paramref name="useOracle"/> to true will use SQL as database. </para>
        /// </summary>
        public UnitOfWork(bool useOracle = true)
        {
            var config = new ConfigurationBuilder()
                .AddJsonFile("appsettings.json", false)
                .Build();
            var dbConStr = config.GetConnectionString("DefaultConnection");
            var optionsBuilder = new DbContextOptionsBuilder<TContext>();
            optionsBuilder.UseLoggerFactory(NlogLoggerFactory);
            if (useOracle)
            {
                optionsBuilder.UseOracle(dbConStr);
            }
            else
            {
                optionsBuilder.UseSqlServer(dbConStr);
            }
            Context = (TContext)Activator.CreateInstance(typeof(TContext), optionsBuilder.Options);
            Context.Database.EnsureCreated();
        }
        /// <summary>
        /// The constructor gets the connection string from its first parameter and set it into TContext's connection.
        /// <para>setting <paramref name="useOracle"/> to true will use Oracle as database. </para>
        /// <para>setting <paramref name="useOracle"/> to true will use SQL as database. </para>
        /// </summary>
        public UnitOfWork(string dbConStr, bool useOracle = true)
        {
            var optionsBuilder = new DbContextOptionsBuilder<TContext>();
            optionsBuilder.UseLoggerFactory(NlogLoggerFactory);
            if (useOracle)
            {
                optionsBuilder.UseOracle(dbConStr);
            }
            else
            {
                optionsBuilder.UseSqlServer(dbConStr);
            }

            Context = (TContext)Activator.CreateInstance(typeof(TContext), optionsBuilder.Options);
            Context.Database.EnsureCreated();
        }
        /// <summary>
        /// Synchronously, saves all changes made in the UnitOfWork's DbContext to the database.
        /// </summary>
        public int SaveChanges()
        {
            foreach (var entity in Context.ChangeTracker.Entries().Where(e => e.State == EntityState.Modified || e.State == EntityState.Added))
            {
                var saveEntity = entity.Entity as BaseEntity;
                if (entity.State == EntityState.Added)
                {
                    saveEntity.Version = 0;
                    saveEntity.DataState = DataState.Active;
                }
                else if (entity.State == EntityState.Modified)
                {
                    saveEntity.Version += 1;
                }
            }
            return Context.SaveChanges();
        }
        /// <summary>
        /// Asynchronously, saves all changes made in the UnitOfWork's DbContext to the database.
        /// </summary>
        public async Task<int> SaveChangesAsync()
        {
            foreach (var entity in Context.ChangeTracker.Entries().Where(e => e.State == EntityState.Modified || e.State == EntityState.Added))
            {
                var saveEntity = entity.Entity as BaseEntity;
                if (entity.State == EntityState.Added)
                {
                    saveEntity.Version = 0;
                    saveEntity.DataState = DataState.Active;
                }
                else if (entity.State == EntityState.Modified)
                {
                    saveEntity.Version += 1;
                }
            }
            return await Context.SaveChangesAsync().ConfigureAwait(false);
        }
        /// <summary>
        /// Transactions allow several database operations to be processed in an atomic manner.
        /// <para>Nested transaction is not allowed in Entity Framework Core. </para>
        /// <para>Usage: unitOfWork.BeginTransaction() </para>
        /// </summary>
        public void BeginTransaction()
        {
            if (Context.Database.CurrentTransaction != null)
            {
                throw new MultipleTransactionException("There is a started Transaction detected, please use the other transaction instead of starting a new Transaction.");
            }
            Context.Database.BeginTransaction();
        }
        /// <summary>
        /// Synchronously, save all the changes done in the transaction & commit the transaction into the database
        /// <para>Nested transaction is not allowed in Entity Framework Core. </para>
        /// <para>Usage: unitOfWork.CommitTransaction() </para>
        /// </summary>
        public void CommitTransaction()
        {
            if (Context.Database.CurrentTransaction == null)
            {
                throw new MultipleTransactionException("Transaction does not exists. Please create one with UnitOfWork.BeginTransaction()");
            }
            SaveChanges();
            Context.Database.CommitTransaction();
        }
        /// <summary>
        /// Asynchronously, save all the changes done in the transaction & commit the transaction into the database
        /// <para>Nested transaction is not allowed in Entity Framework Core. </para>
        /// <para>Usage: unitOfWork.CommitTransaction() </para>
        /// </summary>
        public async Task CommitTransactionAsync()
        {
            if (Context.Database.CurrentTransaction == null)
            {
                throw new MultipleTransactionException("Transaction does not exists. Please create one with UnitOfWork.BeginTransaction()");
            }
            await SaveChangesAsync().ConfigureAwait(false);
            Context.Database.CommitTransaction();
        }
        /// <summary>
        /// Rollback all of the changes done in the Transaction
        /// <para>Nested transaction is not allowed in Entity Framework Core. </para>
        /// <para>Usage: unitOfWork.RollbackTransaction() </para>
        /// </summary>
        public void RollbackTransaction()
        {
            if (Context.Database.CurrentTransaction == null)
            {
                throw new MultipleTransactionException("Transaction does not exists. Please create one with UnitOfWork.BeginTransaction()");
            }
            Context.Database.RollbackTransaction();
        }
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                Context.Dispose();
            }
        }
    }
}
